// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview.test;

import android.content.Context;
import android.support.test.InstrumentationRegistry;

import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwGeolocationPermissions;
import org.chromium.android_webview.AwSettings;
import org.chromium.android_webview.test.AwActivityTestRule.TestDependencyFactory;
import org.chromium.base.test.util.Feature;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.device.geolocation.LocationProviderOverrider;
import org.chromium.device.geolocation.MockLocationProvider;

import java.util.concurrent.Callable;

/**
 * Test suite for Geolocation in AwContents. Smoke tests for
 * basic functionality, and tests to ensure the AwContents.onPause
 * and onResume APIs affect Geolocation as expected.
 */
@RunWith(AwJUnit4ClassRunner.class)
public class GeolocationTest {
    @Rule
    public AwActivityTestRule mActivityTestRule = new AwActivityTestRule() {
        @Override
        public TestDependencyFactory createTestDependencyFactory() {
            return mOverridenFactory == null ? new TestDependencyFactory() : mOverridenFactory;
        }
    };

    private TestAwContentsClient mContentsClient;
    private AwContents mAwContents;
    private MockLocationProvider mMockLocationProvider;
    private TestDependencyFactory mOverridenFactory;

    private static final String RAW_HTML =
            "<!DOCTYPE html>\n"
            + "<html>\n"
            + "  <head>\n"
            + "    <title>Geolocation</title>\n"
            + "    <script>\n"
            + "      var positionCount = 0;\n"
            + "      function gotPos(position) {\n"
            + "        positionCount++;\n"
            + "      }\n"
            + "      function errorCallback(error){"
            + "        window.document.title = 'deny';"
            + "        console.log('navigator.getCurrentPosition error: ', error);"
            + "      }"
            + "      function initiate_getCurrentPosition() {\n"
            + "        navigator.geolocation.getCurrentPosition(\n"
            + "            gotPos, errorCallback, { });\n"
            + "      }\n"
            + "      function initiate_watchPosition() {\n"
            + "        navigator.geolocation.watchPosition(\n"
            + "            gotPos, errorCallback, { });\n"
            + "      }\n"
            + "    </script>\n"
            + "  </head>\n"
            + "  <body>\n"
            + "  </body>\n"
            + "</html>";

    private static class GrantPermisionAwContentClient extends TestAwContentsClient {
        @Override
        public void onGeolocationPermissionsShowPrompt(
                String origin, AwGeolocationPermissions.Callback callback) {
            callback.invoke(origin, true, true);
        }
    }

    private static class DefaultPermisionAwContentClient extends TestAwContentsClient {
        @Override
        public void onGeolocationPermissionsShowPrompt(
                String origin, AwGeolocationPermissions.Callback callback) {
            // This method is empty intentionally to simulate callback is not referenced.
        }
    }

    private void initAwContents(TestAwContentsClient contentsClient) {
        mContentsClient = contentsClient;
        mAwContents = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient)
                              .getAwContents();
        AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
        InstrumentationRegistry.getInstrumentation().runOnMainSync(
                () -> mAwContents.getSettings().setGeolocationEnabled(true));
    }

    @Before
    public void setUp() {
        mMockLocationProvider = new MockLocationProvider();
        LocationProviderOverrider.setLocationProviderImpl(mMockLocationProvider);
    }

    @After
    public void tearDown() {
        mMockLocationProvider.stopUpdates();
        mOverridenFactory = null;
    }

    private int getPositionCountFromJS() {
        int result = -1;
        try {
            result = Integer.parseInt(mActivityTestRule.executeJavaScriptAndWaitForResult(
                    mAwContents, mContentsClient, "positionCount"));
        } catch (Exception e) {
            Assert.fail("Unable to get positionCount");
        }
        return result;
    }

    private void ensureGeolocationRunning(final boolean running) {
        AwActivityTestRule.pollInstrumentationThread(
                () -> mMockLocationProvider.isRunning() == running);
    }

    private static class GeolocationOnInsecureOriginsTestDependencyFactory
            extends TestDependencyFactory {
        private boolean mAllow;
        public GeolocationOnInsecureOriginsTestDependencyFactory(boolean allow) {
            mAllow = allow;
        }

        @Override
        public AwSettings createAwSettings(Context context, boolean supportLegacyQuirks) {
            return new AwSettings(context, false /* isAccessFromFileURLsGrantedByDefault */,
                    supportLegacyQuirks, false /* allowEmptyDocumentPersistence */, mAllow,
                    false /* doNotUpdateSelectionOnMutatingSelectionRange */);
        }
    }

    /**
     * Ensure that a call to navigator.getCurrentPosition works in WebView.
     */
    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testGetPosition() throws Throwable {
        initAwContents(new GrantPermisionAwContentClient());
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "https://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests(
                                "initiate_getCurrentPosition();", null));

        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() == 1);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests(
                                "initiate_getCurrentPosition();", null));
        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() == 2);
    }

    /**
     * Ensure that a call to navigator.watchPosition works in WebView.
     */
    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testWatchPosition() throws Throwable {
        initAwContents(new GrantPermisionAwContentClient());
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "https://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests("initiate_watchPosition();", null));

        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() > 1);
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testPauseGeolocationOnPause() throws Throwable {
        initAwContents(new GrantPermisionAwContentClient());
        // Start a watch going.
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "https://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests("initiate_watchPosition();", null));

        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() > 1);

        ensureGeolocationRunning(true);

        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mAwContents.onPause());

        ensureGeolocationRunning(false);

        try {
            mActivityTestRule.executeJavaScriptAndWaitForResult(
                    mAwContents, mContentsClient, "positionCount = 0");
        } catch (Exception e) {
            Assert.fail("Unable to clear positionCount");
        }
        Assert.assertEquals(0, getPositionCountFromJS());

        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mAwContents.onResume());

        ensureGeolocationRunning(true);

        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() > 1);
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testPauseAwContentsBeforeNavigating() throws Throwable {
        initAwContents(new GrantPermisionAwContentClient());
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mAwContents.onPause());

        // Start a watch going.
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "https://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests("initiate_watchPosition();", null));

        Assert.assertEquals(0, getPositionCountFromJS());

        ensureGeolocationRunning(false);

        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mAwContents.onResume());

        ensureGeolocationRunning(true);

        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() > 1);
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    public void testResumeWhenNotStarted() throws Throwable {
        initAwContents(new GrantPermisionAwContentClient());
        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mAwContents.onPause());

        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "https://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mAwContents.onResume());

        ensureGeolocationRunning(false);
    }

    @Test
    @Feature({"AndroidWebView"})
    @SmallTest
    public void testDenyAccessByDefault() throws Throwable {
        initAwContents(new DefaultPermisionAwContentClient());
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "https://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests(
                                "initiate_getCurrentPosition();", null));

        AwActivityTestRule.pollInstrumentationThread(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                Runtime.getRuntime().gc();
                return "deny".equals(mActivityTestRule.getTitleOnUiThread(mAwContents));
            }
        });
    }

    @Test
    @Feature({"AndroidWebView"})
    @SmallTest
    public void testDenyOnInsecureOrigins() throws Throwable {
        mOverridenFactory = new GeolocationOnInsecureOriginsTestDependencyFactory(false);
        initAwContents(new GrantPermisionAwContentClient());
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "http://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests(
                                "initiate_getCurrentPosition();", null));

        AwActivityTestRule.pollInstrumentationThread(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                Runtime.getRuntime().gc();
                return "deny".equals(mActivityTestRule.getTitleOnUiThread(mAwContents));
            }
        });
    }

    @Test
    @Feature({"AndroidWebView"})
    @SmallTest
    public void testAllowOnInsecureOriginsByDefault() throws Throwable {
        initAwContents(new GrantPermisionAwContentClient());
        mActivityTestRule.loadDataWithBaseUrlSync(mAwContents,
                mContentsClient.getOnPageFinishedHelper(), RAW_HTML, "text/html", false,
                "http://google.com/", ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        TestThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.evaluateJavaScriptForTests(
                                "initiate_getCurrentPosition();", null));

        AwActivityTestRule.pollInstrumentationThread(() -> getPositionCountFromJS() > 0);
    }

}
